"""Predator–Prey capture computation utilities.

This module provides a single public function `compute_capture` that determines
whether a predator can capture a prey under the following assumptions:

1. Both predator and prey are point masses that move in a 2-D Euclidean plane.
2. The predator is initially located at (X0, Y0) and the prey at (X1, Y1).
3. At t = 0 the prey begins a uniform-velocity straight-line motion with
   speed `vq` along a direction given by the unit vector (ux, uy).
4. The predator can instantaneously steer and thereafter flies at constant
   speed `vp`, attempting to intercept the prey.
5. A capture is registered once the Euclidean distance between the two becomes
   not greater than the predator's capture radius `R`.

The function returns a dictionary that states whether capture is possible and,
if so, the time and position of the first capture event.

All distances are expressed in metres (m) and all speeds in metres per second
(m/s).  By default the capture radius corresponds to 40 km (40_000 m) and both
agents travel at 10 m/s.
"""
from __future__ import annotations

import math
from typing import Dict, Tuple, Optional

__all__ = ["compute_capture"]


# Small tolerance used when comparing floating-point numbers to zero.
_EPS = 1e-9


def _normalize(vx: float, vy: float) -> Tuple[float, float]:
    """Return the unit vector collinear with (vx, vy).

    Raises
    ------
    ValueError
        If the input vector is the zero vector.
    """
    norm = math.hypot(vx, vy)
    if norm < _EPS:
        raise ValueError("Direction vector must be non-zero.")
    return vx / norm, vy / norm


def compute_capture(
    X0: float,
    Y0: float,
    X1: float,
    Y1: float,
    ux: float,
    uy: float,
    *,
    R: float = 40_000.0,
    vp: float = 10.0,
    vq: float = 10.0,
) -> Dict[str, Optional[object]]:
    """Determine whether the predator can capture the prey.

    Parameters
    ----------
    X0, Y0
        Initial coordinates of the predator (metres).
    X1, Y1
        Initial coordinates of the prey (metres).
    ux, uy
        Components of the (not necessarily normalised) direction vector along
        which the prey travels. The vector will be normalised internally.
    R
        Predator capture radius in metres. Defaults to 40 km.
    vp
        Predator cruising speed (m/s). Defaults to 10 m/s.
    vq
        Prey speed (m/s). Defaults to 10 m/s.

    Returns
    -------
    dict
        A dictionary with keys:

        ``capture`` (bool)
            ``True`` if capture is possible, ``False`` otherwise.
        ``t`` (Optional[float])
            Capture time (seconds) or ``None`` if capture impossible.
        ``prey_pos`` (Optional[Tuple[float, float]])
            Coordinates of the prey (equivalently capture) at time *t*, or
            ``None`` if capture impossible.
        ``reason`` (str)
            Empty string on successful capture, otherwise a short explanation
            of why capture cannot occur.
    """

    # Normalise the prey direction vector.
    ux_n, uy_n = _normalize(ux, uy)

    # Vector from predator to prey at t = 0.
    dx0 = X1 - X0
    dy0 = Y1 - Y0
    d0_sq = dx0 * dx0 + dy0 * dy0
    d0 = math.sqrt(d0_sq)

    # Immediate capture check.
    if d0 <= R + _EPS:
        return {
            "capture": True,
            "t": 0.0,
            "prey_pos": (X1, Y1),
            "reason": "initial distance within capture radius",
        }

    # Scalar projection of D0 onto prey direction.
    s = dx0 * ux_n + dy0 * uy_n  # dot product D0 · û

    # Coefficients of the quadratic equation: α t² + β t + γ = 0
    alpha = vq * vq - vp * vp
    beta = 2.0 * (vq * s - vp * R)
    gamma = d0_sq - R * R

    # Handle the degenerate linear case when |alpha| is extremely small.
    if abs(alpha) < _EPS:
        # Equation reduces to: beta * t + gamma = 0 → t = -gamma / beta
        if abs(beta) < _EPS:  # beta ~ 0 also ⇒ parallel trajectories
            return {
                "capture": False,
                "t": None,
                "prey_pos": None,
                "reason": "relative speed zero ‑ cannot close distance",
            }

        t_star = -gamma / beta
        if t_star < -_EPS:
            return {
                "capture": False,
                "t": None,
                "prey_pos": None,
                "reason": "linear solution negative time",
            }

    else:
        # Solve quadratic.  Discriminant:
        disc = beta * beta - 4.0 * alpha * gamma
        if disc < 0.0:
            return {
                "capture": False,
                "t": None,
                "prey_pos": None,
                "reason": "no real solution to interception equation",
            }
        sqrt_disc = math.sqrt(disc)
        # Select the smallest non-negative root.
        t1 = (-beta - sqrt_disc) / (2.0 * alpha)
        t2 = (-beta + sqrt_disc) / (2.0 * alpha)
        candidates = [t for t in (t1, t2) if t >= -_EPS]
        if not candidates:
            return {
                "capture": False,
                "t": None,
                "prey_pos": None,
                "reason": "both interception times negative",
            }
        t_star = min(candidates)

    # Ensure time is non-negative.
    t_star = max(0.0, t_star)

    # Capture position is prey position at time t*.
    prey_x = X1 + vq * ux_n * t_star
    prey_y = Y1 + vq * uy_n * t_star

    return {
        "capture": True,
        "t": t_star,
        "prey_pos": (prey_x, prey_y),
        "reason": "",
    }
